package physics;
import build.Simulation;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;


/**
 * @author Robert C. Duvall edited by Sam Rang and Kennan Raju
 */
public class Mass {
    private static final int XDIM = 16;
    private static final int YDIM = 16;
    private static final int BOUNCE_FORCE = 2;
    private static final double DEFAULT_FORCE = 1.0;
    private static final int QUARTER_CIRCLE = 90;
    private static final int TOP_WALL = 1;
    private static final int RIGHT_WALL = 2;
    private static final int BOTTOM_WALL = 3;
    private static final int LEFT_WALL = 4;
    private static final int RESIZE_SCALE = 10;
    private static double ourGravityMagnitude;
    private static double ourGravityAngle;
    private static double ourViscosityMagnitude;
    private static List<Integer> ourWallNumber;
    private static double ourWallMagnitude;
    private static double ourWallExponent;
    private static double ourCenterMassMagnitude;
    private static double ourCenterMassExponent;
    private static boolean ourGravityON;
    private static boolean ourViscosityON;
    private static boolean ourCenterMassON;
    private static int ourResizeCounter;
    private Point2D myCenterOfMass;
    private Point2D myCenter;
    private Force myVelocity;
    private Dimension mySize;
    private int myID;
    private double myMass;
    private Force myAcceleration;

    /**
     * Most of the behavior for the Assembly is determined
     * from this class. The mass sums the forces from its
     * connectors and the environment including gravity,
     * viscosity, center of mass etc.
     * @param id The identification
     * @param x The x location
     * @param y The y location
     * @param mass The amount of mass
     */
    public Mass(int id, double x, double y, double mass) {
        ourResizeCounter = 0;
        ourWallNumber = new ArrayList<Integer>();
        myAcceleration = new Force();
        myMass = mass;
        myID = id;
        setCenter(x, y);
        setVelocity(0, 0);
        mySize = new Dimension(XDIM, YDIM);
    }

    /**
     * This is the paint function that will paint the Mass.
     * @param pen The Graphics which are being written to
     */
    public void paint (Graphics2D pen) {
        pen.setColor(Color.BLACK);
        pen.fillOval(getLeft(), getTop(), getSize().width, getSize().height);
    }
    /**
     * Updates the canvas based the forces exerted on it.
     * Sums the forces and then appropriately sets the velocity
     * and acceleration.
     * @param canvas The Board/GUI which will display images.
     * @param dt The Change in time since the last update
     * @param assembly Assembly the mass belongs to
     */
    public void update (Simulation canvas, double dt, Assembly assembly) {
        applyForce(getBounce(canvas.getSize()));
        if (ourGravityON) {
            applyForce(getGravity());
        }
        if (ourViscosityON) {
            applyForce(getViscosity());
        }
        if (ourCenterMassON) {
            applyForce(getCenterMass(assembly));
        }
        for (Integer w : ourWallNumber) {
            applyForce(getWall(canvas, w));
        }
        // convert force back into Mover's velocity
        getVelocity().sum(myAcceleration);
        myAcceleration.reset();
        // move mass by velocity
        myCenter.setLocation(myCenter.getX() + myVelocity.getXChange() * dt,
                myCenter.getY() + myVelocity.getYChange() * dt);
    }

    /**
     * Apply a force to the mass.
     * @param f force to be applied to this mass
     */
    public void applyForce (Force f) {
        myAcceleration.sum(f);
    }
    /**
     * Checks whether the ID's match.
     * @param id The identification of a mass
     * @return
     */
    public boolean match (int id) {
        return myID == id;
    }

    /**
     * Gets the mass of the desired mass.
     */
    public double getMass () {
        return myMass;
    }

    /**
     * Sets the default value for forces in case the environment
     * didn't contain all the parameters.
     */
    public static void setDefault () {
        setGravity(QUARTER_CIRCLE, DEFAULT_FORCE);
        setViscosity(DEFAULT_FORCE);
        setCenterMass(DEFAULT_FORCE, DEFAULT_FORCE);
    }
    /**
     * Non-static method that returns the force of gravity
     * based on the environment settings.
     */
    private Force getGravity () {
        Force result = new Force(ourGravityAngle, ourGravityMagnitude);
        result.scale(1.0 / myMass);
        return result;
    }

    /**
     * Sets the global force gravity for the whole class.
     * @param gravMag Magnitude of gravitational force
     * @param gravAngle Angle of force of gravity
     */
    public static void setGravity (double gravAngle, double gravMag) {
        ourGravityON = true;
        ourGravityMagnitude = gravMag;
        ourGravityAngle = gravAngle;
    }

    /**
     * Non-static method that returns the scaled force of viscosity
     * for the environment settings.
     */
    private Force getViscosity () {
        Force temp = new Force(getVelocity());
        temp.negate();
        Force vis = new Force(
                temp.getDirection() , ourViscosityMagnitude);
        vis.scale(myVelocity.getMagnitude());
        return vis;
    }

    /**
     * Sets the Viscosity of the environment for entire class.
     * @param viscMag Magnitude of the viscosity,
     * which will be scaled by the mass of the
     * object, that the mass will experience.
     */
    public static void setViscosity (double viscMag) {
        ourViscosityON = true;
        ourViscosityMagnitude = viscMag;
    }

    /**
     * Applies force towards the center of mass of this
     * assembly.
     * @param myAssembly Assembly the mass belongs to
     */
    private Force getCenterMass (Assembly myAssembly) {
        List<Mass> list = myAssembly.getMasses();
        double totalMass = 0;
        double xComponent = 0;
        double yComponent = 0;
        for (Mass m : list) {
            totalMass += m.getMass();
            xComponent += m.getMass() * m.getCenter().getX();
            yComponent += m.getMass() * m.getCenter().getY();
        }
        xComponent /= totalMass;
        yComponent /= totalMass;
        myCenterOfMass = new Point2D.Double(xComponent, yComponent);
        Force f = new Force(
                Force.angleBetween(myCenter, myCenterOfMass),
                ourCenterMassMagnitude / Math.pow(Force.distanceBetween(
                        myCenterOfMass, myCenter), ourCenterMassExponent));
        return f;
    }

    /**
     * Sets the force for center of mass for the assembly.
     * @param centerMassMag magnitude of the center of mass force
     * @param centerMassEx exponent value for the center of mass force
     * each list represents an assembly
     */
    public static void setCenterMass (
            double centerMassMag, double centerMassEx) {
        ourCenterMassON = true;
        ourCenterMassMagnitude = centerMassMag;
        ourCenterMassExponent = centerMassEx;
    }

    /**
     * Returns distance from the wall. Used for
     * the wall force function.
     * @param canvas Current Simulation
     * @return
     */
    private double getDistFromWall (Simulation canvas, Integer w) {
        switch (w) {
            case TOP_WALL:
                return myCenter.getY();
            case RIGHT_WALL:
                return myCenter.getX() - canvas.getSize().getWidth();
            case BOTTOM_WALL:
                return canvas.getSize().getHeight() - myCenter.getY();
            case LEFT_WALL:
                return myCenter.getX();
            default:
                return 0.0;
        }
    }

    /**
     * Returns the Wall force based on the distance of the mass from the wall.
     * @param canvas This simulation
     * @return
     */
    private Force getWall (Simulation canvas, Integer w) {
        Force wallForce = new Force(w * QUARTER_CIRCLE, ourWallMagnitude
                / Math.pow(getDistFromWall(canvas, w), ourWallExponent));
        return wallForce;
    }

    /**
     * Sets the force for the wall.
     * @param wallID Identifies wall
     * @param wallMag Magnitude of wall force
     * @param wallEx Exponent of wall force
     */
    public static void setWall(Integer wallID, double wallMag, double wallEx) {
        if (!ourWallNumber.contains(wallID)) {
            ourWallNumber.add(wallID);
        }
        else {
            ourWallNumber.remove(wallID);
        }
        ourWallMagnitude = wallMag;
        ourWallExponent = wallEx;
    }


    /**
     * Adds impulse force to mass if it reaches any of the walls. The force
     * is determined by the Wall ID and multiplied by 90.
     * @param bounds The dimensions of the canvas
     * @return
     */
    private Force getBounce (Dimension bounds) {
        Force impulse = new Force();
        int offset = ourResizeCounter * RESIZE_SCALE / 2;
        if (getLeft() < 0 + offset) {
            impulse = new Force(LEFT_WALL * QUARTER_CIRCLE, BOUNCE_FORCE);
            setCenter(getSize().width / 2 + offset, getCenter().getY());
        }
        else if (getRight() > bounds.width - offset) {
            impulse = new Force(RIGHT_WALL * QUARTER_CIRCLE, BOUNCE_FORCE);
            setCenter(bounds.width - getSize().width / 2 - offset,
                    getCenter().getY());
        }
        if (getTop() < 0 + ourResizeCounter * RESIZE_SCALE / 2) {
            impulse = new Force(TOP_WALL * QUARTER_CIRCLE, BOUNCE_FORCE);
            setCenter(getCenter().getX(), getSize().height / 2 + offset);
        }
        else if (getBottom() > bounds.height - offset) {
            impulse = new Force(BOTTOM_WALL * QUARTER_CIRCLE, BOUNCE_FORCE);
            setCenter(getCenter().getX(),
                    bounds.height - getSize().height / 2 - offset);
        }
        impulse.scale(getVelocity().getRelativeMagnitude(impulse));
        return impulse;
    }

    /**
     * Returns mass's velocity.
     */
    public Force getVelocity () {
        return myVelocity;
    }

    /**
     * Resets mass's velocity.
     * @param direction The direction of the mass's velocity
     * @param magnitude The strength of the mass's velocity
     */
    public void setVelocity (double direction, double magnitude) {
        myVelocity = new Force(direction, magnitude);
    }

    /**
     * Returns mass's center.
     */
    public Point2D getCenter () {
        return myCenter;
    }

    /**
     * Resets mass's center.
     * @param x The x coordinate of the center of the mass
     * @param y The y coordinate of the center of the mass
     */
    public void setCenter (double x, double y) {
        myCenter = new Point2D.Double(x, y);
    }

    /**
     * Returns mass's left-most coordinate.
     */
    public int getLeft () {
        return (int) (getCenter().getX() - getSize().width / 2);
    }

    /**
     * Returns mass's top-most coordinate.
     */
    public int getTop () {
        return (int) (getCenter().getY() - getSize().height / 2);
    }

    /**
     * Returns mass's right-most coordinate.
     */
    public int getRight () {
        return (int) (getCenter().getX() + getSize().width / 2);
    }

    /**
     * Reports mass's bottom-most coordinate.
     * @return bottom-most coordinate
     */
    public int getBottom () {
        return (int) (getCenter().getY() + getSize().height / 2);
    }

    /**
     * Returns shape's size.
     */
    public Dimension getSize () {
        return mySize;
    }

    /**
     * Shared setter for the global forces.
     * @param g gravity
     * @param v viscosity
     * @param m center of mass
     */
    public static void setForce (boolean g, boolean v, boolean m) {
        if (g) {
            ourGravityON = !ourGravityON;
        }
        else if (v) {
            ourViscosityON = !ourViscosityON;
        }
        else if (m) {
            ourCenterMassON = !ourCenterMassON;
        }
    }

    /**
     * Shared setter for toggling different wall forces.
     * @param wall Wall identification number
     */
    public static void addWall (Integer wall) {
        if (!ourWallNumber.contains(wall)) {
            ourWallNumber.add(wall);
        }
        else {
            ourWallNumber.remove(wall);
        }
    }

    /**
     * Increments or decrements the resize counter.
     * @param i either positive or negative 1
     */
    public static void resizeIterate (int i) {
        ourResizeCounter += i;
    }
}
